Skip to content

Conversation

ooheunda
Copy link
Contributor

@ooheunda ooheunda commented Oct 3, 2025

🔥 변경 사항

주간 뉴스레터 수신 거부 API(/user/newsletter-unsubscribe)를 추가하였습니다.

  • 뉴스레터 템플릿에서 "수신 거부" 버튼을 통해 API에 직접 접근 (GET)
  • 구독 해제후 /main으로 리디렉트

🏷 관련 이슈

📸 스크린샷 (UI 변경 시 필수)

X

📌 체크리스트

  • 기능이 정상적으로 동작하는지 테스트 완료
  • 코드 스타일 가이드 준수 여부 확인
  • 관련 문서 업데이트 완료 (필요 시)

Summary by CodeRabbit

  • 신규 기능
    • 뉴스레터 구독 해지 엔드포인트(/newsletter-unsubscribe) 추가: 이메일 쿼리로 구독 해지 시도 후 /main으로 리다이렉트합니다.
  • 테스트
    • 구독 해지 관련 단위 테스트 추가: 이메일 누락·유효성 실패·정상 해지 시나리오 검증.
  • 문서
    • 새 엔드포인트에 대한 Swagger/OpenAPI 문서 추가(경로, 쿼리 파라미터, 302 응답).
  • 기타
    • 사용자 모델에 구독 상태 필드 및 목 데이터 추가; 서비스·저장소의 관련 로직 보강.

@ooheunda ooheunda self-assigned this Oct 3, 2025
@ooheunda ooheunda added the enhancement New feature or request label Oct 3, 2025
Copy link

Copy link
Contributor

coderabbitai bot commented Oct 3, 2025

Walkthrough

뉴스레터 구독 해지 흐름을 추가했습니다. GET /newsletter-unsubscribe 라우트와 컨트롤러 핸들러 unsubscribeNewsletter를 도입하고, 서비스/레포지토리에 이메일 조회 및 구독 해지 로직, 타입·픽스처·테스트·Swagger 문서를 추가했습니다.

Changes

Cohort / File(s) Summary
Controller
src/controllers/user.controller.ts
unsubscribeNewsletter: RequestHandler 추가: query에서 email 추출, 정규식 검증(유효하지 않으면 로깅 후 /main 리다이렉트), 유효하면 userService.unsubscribeNewsletter(email) 호출, 오류는 로깅 후 next로 전달. 임포트 경로 정리.
Controller Tests
src/controllers/__test__/user.controller.test.ts
unsubscribeNewsletter 테스트 추가(이메일 누락, 이메일 형식 불일치, 정상 해지): 서비스 호출 여부와 /main 리다이렉트, next 호출 미발생 검증.
Service
src/services/user.service.ts
unsubscribeNewsletter(email: string) 추가: findByUserEmail로 사용자 조회, 없으면 로깅 후 반환(예외 미발생), 있으면 repository.unsubscribeNewsletter(user.id) 호출; 내부 오류는 로깅 후 재throw.
Repository
src/repositories/user.repository.ts
findByUserId / findByUserVelogUUID 반환형을 Promise<User | null>로 변경. findByUserEmail(email: string): Promise<User | null>unsubscribeNewsletter(id: number): Promise<void> 추가(업데이트 쿼리 및 오류 처리).
Router & Docs
src/routes/user.router.ts
GET /newsletter-unsubscribe 라우트 추가 및 Swagger/OpenAPI 문서(태그·요약·query param email·302 응답) 추가.
Types
src/types/models/User.type.ts
User 인터페이스에 newsletter_subscribed: boolean 필드 추가.
Fixtures
src/utils/fixtures.ts
mockUsernewsletter_subscribed: true 필드 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant R as Router
  participant C as UserController
  participant S as UserService
  participant Repo as UserRepository
  participant DB as Database

  U->>R: GET /newsletter-unsubscribe?email=...
  R->>C: unsubscribeNewsletter(req)
  alt 이메일 누락
    C-->>U: 302 Redirect /main (로깅, next 미호출)
  else 이메일 존재
    C->>S: unsubscribeNewsletter(email)
    S->>Repo: findByUserEmail(email)
    Repo->>DB: SELECT user WHERE email=?
    DB-->>Repo: User | null
    alt 사용자 없음
      Repo-->>S: null (로그 후 반환)
      S-->>C: void
      C-->>U: 302 Redirect /main
    else 사용자 있음
      S->>Repo: unsubscribeNewsletter(user.id)
      Repo->>DB: UPDATE newsletter_subscribed=false
      DB-->>Repo: OK
      Repo-->>S: void
      S-->>C: void
      C-->>U: 302 Redirect /main
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • six-standard

Poem

깡총, 편지함 한 줄기 바람에 귀 쫑긋 🐇
링크를 밟아 이메일을 전해주었네, 휙—
구독은 꺼지고, 리다이렉트는 /main.
당분간 고요한 인박스에서 당근을 꿈꾸네. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목이 PR의 핵심 변경 사항인 주간 뉴스레터 수신거부 API 추가를 명확하고 간결하게 요약하고 있어 리뷰 히스토리에서 주요 기능을 바로 파악할 수 있습니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/newsletter-unsubscription

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41e3ac0 and de08d82.

📒 Files selected for processing (2)
  • src/controllers/__test__/user.controller.test.ts (1 hunks)
  • src/controllers/user.controller.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/controllers/test/user.controller.test.ts
  • src/controllers/user.controller.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/repositories/user.repository.ts (1)

166-174: 업데이트 검증 로직 추가 고려

현재 구현은 쿼리 실행 후 업데이트된 행의 수를 확인하지 않습니다. 존재하지 않는 사용자 ID가 전달되어도 오류 없이 성공으로 처리됩니다.

이것이 의도된 동작인지 확인해주세요. 만약 존재하지 않는 사용자에 대한 명시적인 에러 처리가 필요하다면 다음과 같이 개선할 수 있습니다:

 async unsubscribeNewsletter(id: number): Promise<void> {
   try {
-    const query = `UPDATE "users_user" SET newsletter_subscribed = false WHERE id = $1`;
-    await this.pool.query(query, [id]);
+    const query = `UPDATE "users_user" SET newsletter_subscribed = false WHERE id = $1 RETURNING id`;
+    const result = await this.pool.query(query, [id]);
+    if (result.rowCount === 0) {
+      throw new DBError('해당 사용자를 찾을 수 없습니다.');
+    }
   } catch (error) {
     logger.error('User Repo unsubscribeNewsletter Error : ', error);
     throw new DBError('뉴스레터 구독 해제 중 문제가 발생했습니다.');
   }
 }
src/routes/user.router.ts (1)

172-172: GET 메서드 사용의 보안 고려사항

이메일에서 직접 클릭하는 구독 해제 링크의 특성상 GET 메서드 사용이 일반적이고 사용자 편의성이 높습니다. 다만 다음의 보안 고려사항이 있습니다:

  • 이메일 스캐너나 브라우저 프리페치가 의도치 않게 구독 해제를 트리거할 수 있음
  • CSRF 보호가 없음
  • 로그나 브라우저 히스토리에 이메일이 노출됨

보안을 강화하려면 다음 접근을 고려할 수 있습니다:

  1. 이메일 대신 일회성 토큰을 쿼리 파라미터로 사용
  2. 토큰 기반으로 사용자를 식별하고 만료 시간 설정
  3. 구독 해제 확인 페이지를 표시하고 POST 요청으로 최종 처리

현재 구현도 일반적인 패턴이므로 비즈니스 요구사항에 따라 판단하시면 됩니다.

src/controllers/__test__/user.controller.test.ts (1)

409-447: 테스트 커버리지 개선 제안

기본적인 성공/실패 케이스는 잘 커버되어 있습니다. 다음의 추가 테스트 케이스를 고려해보세요:

it('서비스에서 에러 발생 시 nextFunction에 에러를 전달해야 한다', async () => {
  const email = 'test@example.com';
  mockRequest.query = { email };
  const error = new Error('Database error');
  mockUserService.unsubscribeNewsletter.mockRejectedValue(error);

  await userController.unsubscribeNewsletter(
    mockRequest as Request,
    mockResponse as Response,
    nextFunction
  );

  expect(nextFunction).toHaveBeenCalledWith(error);
  expect(mockResponse.redirect).not.toHaveBeenCalled();
});

it('이메일이 빈 문자열인 경우 BadRequestError를 던져야 한다', async () => {
  mockRequest.query = { email: '' };

  await userController.unsubscribeNewsletter(
    mockRequest as Request,
    mockResponse as Response,
    nextFunction
  );

  expect(nextFunction).toHaveBeenCalledWith(
    expect.objectContaining({
      message: '이메일이 필요합니다.',
    })
  );
});
src/controllers/user.controller.ts (1)

173-186: 이메일 검증 및 PII 로깅 개선 고려

현재 구현은 이메일 존재 여부만 확인하고 형식은 검증하지 않습니다. 또한 오류 로그에 이메일이 노출됩니다.

다음 개선 사항들을 고려해보세요:

1. 이메일 형식 검증 추가 (선택사항):

if (!email || typeof email !== 'string' || !email.includes('@')) {
  throw new BadRequestError('올바른 이메일 형식이 아닙니다.');
}

2. PII 로깅 최소화:

   } catch (error) {
-    logger.error(`뉴스레터 구독 해제 실패: [email: ${req.query.email}]`, error);
+    logger.error(`뉴스레터 구독 해제 실패`, error);
     next(error);
   }

현재 구현도 기본적인 요구사항을 충족하지만, 위 개선 사항들은 보안과 개인정보 보호를 강화할 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e29d2ba and e654529.

📒 Files selected for processing (7)
  • src/controllers/__test__/user.controller.test.ts (1 hunks)
  • src/controllers/user.controller.ts (2 hunks)
  • src/repositories/user.repository.ts (4 hunks)
  • src/routes/user.router.ts (1 hunks)
  • src/services/user.service.ts (1 hunks)
  • src/types/models/User.type.ts (1 hunks)
  • src/utils/fixtures.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-12-05T17:21:07.909Z
Learnt from: HA0N1
PR: Check-Data-Out/velog-dashboard-v2-api#6
File: src/reposities/user.repository.ts:7-15
Timestamp: 2024-12-05T17:21:07.909Z
Learning: Function `findByUserEmail` in `src/reposities/user.repository.ts` is scheduled to be deleted and should be ignored in future code reviews.

Applied to files:

  • src/repositories/user.repository.ts
🧬 Code graph analysis (2)
src/controllers/user.controller.ts (2)
src/types/dto/responses/emptyReponse.type.ts (1)
  • EmptyResponseDto (17-17)
src/exception/badRequest.exception.ts (1)
  • BadRequestError (3-7)
src/repositories/user.repository.ts (3)
src/types/models/User.type.ts (1)
  • User (1-16)
src/types/index.ts (1)
  • User (2-2)
src/exception/db.exception.ts (1)
  • DBError (3-7)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-and-test (23)
  • GitHub Check: build-and-test (21)
  • GitHub Check: build-and-test (22)
🔇 Additional comments (7)
src/types/models/User.type.ts (1)

14-15: LGTM!

newsletter_subscribed 필드 추가가 적절합니다. 타입 정의가 명확하고 다른 필드들과 일관성 있게 구성되었습니다.

src/repositories/user.repository.ts (3)

10-10: LGTM!

반환 타입에 | null을 추가하여 타입 안전성이 개선되었습니다. 실제 구현 로직과 타입 시그니처가 일치합니다.

Also applies to: 20-20


30-38: 과거 리뷰 컨텍스트와 충돌 확인 필요

검색된 학습 내용에 따르면 findByUserEmail 함수는 삭제 예정이었습니다. 이 PR에서 해당 메서드를 새로 추가하고 있는데, 요구사항이 변경되었는지 확인이 필요합니다.

Based on learnings.

구현 자체는 올바릅니다:

  • 파라미터화된 쿼리로 SQL 인젝션 방지
  • 일관된 에러 핸들링

168-168: 마이그레이션 및 스키마 검증 필요
newsletter_subscribed 컬럼을 추가하는 마이그레이션 파일이 보이지 않습니다. 실제 DB 스키마에 해당 컬럼과 기본값 설정이 존재하는지 확인해주세요.

src/utils/fixtures.ts (1)

40-40: LGTM!

테스트 픽스처에 newsletter_subscribed 필드가 적절하게 추가되었습니다. 구독 상태(true)로 설정되어 구독 해제 기능 테스트에 적합합니다.

src/routes/user.router.ts (1)

154-171: LGTM!

Swagger 문서가 완전하고 명확합니다. 필수 쿼리 파라미터와 리디렉션 응답이 정확하게 정의되어 있습니다.

src/controllers/user.controller.ts (1)

7-7: LGTM!

BadRequestError 임포트가 적절하게 추가되었습니다.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/services/user.service.ts (1)

178-181: 이전 리뷰에서 이미 지적된 문제입니다.

사용자를 찾지 못한 경우 조용히 실패하는 이 동작은 이전 리뷰에서 이미 논의되었습니다. 보안상 이메일 열거 공격을 방지하려는 의도라면, 해당 설계 결정을 명확히 하는 주석을 추가하는 것이 좋습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e654529 and 41e3ac0.

📒 Files selected for processing (1)
  • src/services/user.service.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/services/user.service.ts (1)
src/modules/slack/slack.notifier.ts (1)
  • sendSlackMessage (23-28)
🔇 Additional comments (1)
src/services/user.service.ts (1)

190-194: Slack 알림 에러 처리 잘 되어 있습니다.

Slack 알림 실패가 메인 비즈니스 로직(구독 해제)에 영향을 주지 않도록 격리된 에러 처리를 적용한 것은 좋은 패턴입니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타입 강화를 더 돈독히 해주셨군요. (애초에 이게 TS 린팅에 안걸리는게 더 신기하네..)

Comment on lines 175 to 178
const email = req.query.email as string;
if (!email) {
throw new BadRequestError('이메일이 필요합니다.');
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 에러를 받으면 그러면 사용자는 어떻게 해야 하나요?

Copy link
Member

@Nuung Nuung left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하나 치명적인 엣지 케이스에 대해 여쭙고 싶습니다.
구독 취소를 한 번 이라도 눌러본 사람이라면, API 호출을 뭘 하는지 누구나 볼 수 있게 될 겁니다.
어떤 사람이 악의를 가지고, 구독 취소 API 뒤에 쿼리 스트링값으로, 부르트포스 공격을 하면 어떻게 되나요?

Copy link
Member

@six-standard six-standard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 잘 읽었습니다!
현우님이 해 주신 질문에 동감하고 있고, 그 외에는 큰 문제는 없는 것 같네요

좋았던 점

  • 타입 지정을 깔끔하게 강화해 주신 점이 좋았습니다!

Copy link
Contributor

@Jihyun3478 Jihyun3478 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰가 너무 늦었네요🥲 고생하셨습니다!!

좋았던 점

  • 타입이 일치하도록 개선해주신 부분이 좋았습니다!
  • 이메일 검증 로직을 추가해 각 단계별로 에러 로깅을 남겨서 추적 가능하게 한 점이 인상적이었습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants